/*******************************************************************************
 * Copyright (c) 2010, 2017 Oak Ridge National Laboratory and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 * 
 * SPDX-License-Identifier: EPL-2.0
 ******************************************************************************/
package org.eclipse.nebula.visualization.xygraph.linearscale;

import java.text.DecimalFormat;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.eclipse.draw2d.Figure;
import org.eclipse.draw2d.TextUtilities;
import org.eclipse.nebula.visualization.internal.xygraph.utils.LargeNumberUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Font;

/**
 * The abstract scale has the common properties for linear(straight) scale and
 * round scale.
 * 
 * @author Xihui Chen
 *
 */
public abstract class AbstractScale extends Figure {

	/** ticks label's position relative to tick marks */
	public enum LabelSide {

		/**
		 * bottom or left side of tick marks for linear scale, or outside for
		 * round scale
		 */
		Primary,

		/**
		 * top or right side of tick marks for linear scale, or inside for round
		 * scale
		 */
		Secondary
	}

	public static final double DEFAULT_MAX = 100d;

	public static final double DEFAULT_MIN = 0d;

	public static final String DEFAULT_ENGINEERING_FORMAT = "0.####E0";//$NON-NLS-1$

	/**
	 * the digits limit to be displayed in engineering format
	 */
	protected static final int ENGINEERING_LIMIT = 4;

	protected static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd\nHH:mm:ss"; //$NON-NLS-1$

	private static final Map<String, Format> formatCache = new HashMap<String, Format>();

	/** ticks label position */
	private LabelSide tickLabelSide = LabelSide.Primary;

	/** the default minimum value of log scale range */
	private final static double DEFAULT_LOG_SCALE_MIN = 0.1d;

	/** the default maximum value of log scale range */
	public final static double DEFAULT_LOG_SCALE_MAX = 100d;

	/** the default label format */
	protected String default_decimal_format = "############.##"; //$NON-NLS-1$

	/** the state if the axis scale is log scale */
	private boolean logScaleEnabled = false;

	/** The minimum value of the scale */
	protected double min = DEFAULT_MIN;

	/** The maximum value of the scale */
	protected double max = DEFAULT_MAX;

	/** the format for tick labels */
	private String formatPattern;

	/** the time unit for tick step */
	private int timeUnit = 0;

	/**
	 * Whenever any parameter has been changed, the scale should be marked as
	 * dirty, so all the inner parameters could be recalculated before the next
	 * paint
	 */
	protected boolean dirty = true;

	private boolean dateEnabled = false;

	private boolean scaleLineVisible = true;

	/** the pixels hint for major tick mark step */
	private int majorTickMarkStepHint = 40;

	/** the pixels hint for minor tick mark step */
	private int minorTickMarkStepHint = 4;

	private boolean minorTicksVisible = true;

	private double majorGridStep = 0;

	private boolean autoFormat = true;

	private Range range = new Range(min, max);

	private int formatPatternSize = 0;

	/**
	 * Formats the given object as a DateFormat if Date is enabled or as a
	 * DecimalFormat. This is based on an internal format pattern given the
	 * object in parameter.
	 *
	 * @param obj
	 *            the object
	 * @return the formatted string
	 */
	public String format(Object obj) {
		return format(obj, false);
	}

	/**
	 * Formats the given object as a DateFormat if Date is enabled or as a
	 * DecimalFormat. This is based on an internal format pattern given the
	 * object in parameter. When formatting a date, if minOrMaxDate is true as
	 * well as autoFormat, then the SimpleDateFormat us used to format the
	 * object.
	 *
	 * @param obj
	 *            the object
	 * @param minOrMaxDate
	 *            true if it is the min or max date on the scale.
	 * @return the formatted string
	 */
	public String format(Object obj, boolean minOrMaxDate) {

		if (isDateEnabled()) {
			if (autoFormat || formatPattern == null || formatPattern.equals("")
					|| formatPattern.equals(default_decimal_format)
					|| formatPattern.equals(DEFAULT_ENGINEERING_FORMAT)) {
				double length = Math.abs(max - min);
				if (length <= 5000 || timeUnit == Calendar.MILLISECOND) { // less
																			// than
																			// five
																			// second
					internalSetFormatPattern("ss.SSS");//$NON-NLS-1$
				} else if (length <= 1800000d || timeUnit == Calendar.SECOND) { // less
																				// than
																				// 30
																				// min
					internalSetFormatPattern("HH:mm:ss");//$NON-NLS-1$
				} else if (length <= 86400000d || timeUnit == Calendar.MINUTE) { // less
																					// than
																					// a
																					// day
					internalSetFormatPattern("HH:mm");//$NON-NLS-1$
				} else if (length <= 604800000d || timeUnit == Calendar.HOUR_OF_DAY) { // less
																						// than
																						// a
																						// week
					internalSetFormatPattern("MM-dd\nHH:mm");//$NON-NLS-1$
				} else if (length <= 2592000000d || timeUnit == Calendar.DATE) { // less
																					// than
																					// a
																					// month
					internalSetFormatPattern("MM-dd");//$NON-NLS-1$
					// } else if (length <= 31536000000d ||timeUnit ==
					// Calendar.MONTH) { //less than a year
					// formatPattern = "yyyy-MM-dd";//$NON-NLS-1$
				} else { // more than a month
					internalSetFormatPattern("yyyy-MM-dd"); //$NON-NLS-1$
				}
				autoFormat = true;
			}
			if (minOrMaxDate && autoFormat) {
				if (Math.abs(max - min) < 5000)
					return new SimpleDateFormat("yyyy-MM-dd\nHH:mm:ss.SSS").format(obj); //$NON-NLS-1$
				return getFormat(DEFAULT_DATE_FORMAT, true).format(obj);
			}
			return getFormat(formatPattern, true).format(obj);
		}

		if (formatPattern == null || formatPattern.equals("")) {
			formatPattern = default_decimal_format;
			autoFormat = true;
		}

		return getFormat(formatPattern, false).format(obj);
	}

	private Format getFormat(String pattern, boolean isDateFormat) {
		Format result = formatCache.get(pattern);
		if (result == null) {
			if (isDateFormat)
				result = new SimpleDateFormat(pattern);
			else
				result = new DecimalFormat(pattern);
			formatCache.put(pattern, result);
		}
		return result;
	}

	/**
	 * @return the majorTickMarkStepHint
	 */
	public int getMajorTickMarkStepHint() {
		if (isDateEnabled()) {
			return Math.max(majorTickMarkStepHint, formatPatternSize);
		}
		return majorTickMarkStepHint;
	}

	/** get the scale range */
	public Range getRange() {
		return range;
	}

	/**
	 * @deprecated use correctly spelled {@link #getTickLabelSide(LabelSide)}
	 */
	@Deprecated
	public LabelSide getTickLablesSide() {
		return getTickLabelSide();
	}

	/**
	 * @return the side of the tick label relative to the tick marks
	 */
	public LabelSide getTickLabelSide() {
		return tickLabelSide;
	}

	/**
	 * @return the timeUnit
	 */
	public int getTimeUnit() {
		return timeUnit;
	}

	/**
	 * @return the dateEnabled
	 */
	public boolean isDateEnabled() {
		return dateEnabled;
	}

	/**
	 * @return the dirty
	 */
	public boolean isDirty() {
		return dirty;
	}

	/**
	 * Gets the state indicating if log scale is enabled.
	 * 
	 * @return true if log scale is enabled
	 */
	public boolean isLogScaleEnabled() {
		return logScaleEnabled;
	}

	/**
	 * @return the minorTicksVisible
	 */
	public boolean isMinorTicksVisible() {
		return minorTicksVisible;
	}

	/**
	 * @return the scaleLineVisible
	 */
	public boolean isScaleLineVisible() {
		return scaleLineVisible;
	}

	/**
	 * @param dateEnabled
	 *            the dateEnabled to set
	 */
	public void setDateEnabled(boolean dateEnabled) {
		this.dateEnabled = dateEnabled;
		setDirty(true);
		revalidate();

	}

	/**
	 * Whenever any parameter has been changed, the scale should be marked as
	 * dirty, so all the inner parameters could be recalculated before the next
	 * paint
	 * 
	 * @param dirty
	 *            the dirty to set
	 */
	protected void setDirty(boolean dirty) {
		this.dirty = dirty;
	}

	/**
	 * Sets the format pattern for axis tick label. see {@link Format}
	 * <p>
	 * If <tt>null</tt> is set, default format will be used.
	 * 
	 * @param format
	 *            the format
	 * @exception NullPointerException
	 *                if <code>pattern</code> is null
	 * @exception IllegalArgumentException
	 *                if the given pattern is invalid.
	 */
	public void setFormatPattern(String formatPattern) {
		try {
			new DecimalFormat(formatPattern);
		} catch (NullPointerException e) {
			throw e;
		} catch (IllegalArgumentException e) {
			throw e;
		}

		internalSetFormatPattern(formatPattern);
		autoFormat = false;
		setDirty(true);
		revalidate();
		repaint();
	}

	protected void internalSetFormatPattern(String formatPattern) {
		if (Objects.equals(this.formatPattern, formatPattern))
			return;
		this.formatPattern = formatPattern;
		if (formatPattern != null && isDateEnabled())
			formatPatternSize = TextUtilities.INSTANCE.getTextExtents(formatPattern, getFont()).width;
	}

	/**
	 * @return the formatPattern
	 */
	public String getFormatPattern() {
		return formatPattern;
	}

	@Override
	public void setFont(Font f) {
		super.setFont(f);
		setDirty(true);
		revalidate();
	}

	/**
	 * @param enabled
	 *            true if enabling log scales
	 * @throws IllegalStateException
	 */
	public void setLogScale(boolean enabled) throws IllegalStateException {

		if (logScaleEnabled == enabled) {
			return;
		}
		if (enabled) {
			if (min == DEFAULT_MIN && max == DEFAULT_MAX) {
				min = DEFAULT_LOG_SCALE_MIN;
				max = DEFAULT_LOG_SCALE_MAX;
			}
			if (min <= 0) {
				min = DEFAULT_LOG_SCALE_MIN;
			}
			if (max <= min) {
				max = min + DEFAULT_LOG_SCALE_MAX;
			}
		} else if (min == DEFAULT_LOG_SCALE_MIN && max == DEFAULT_LOG_SCALE_MAX) {
			min = DEFAULT_MIN;
			max = DEFAULT_MAX;
		}

		logScaleEnabled = enabled;
		range = new Range(min, max);
		setDirty(true);
		revalidate();
		repaint();

	}

	protected void internalSetLogScaleEnabled(boolean logScaleEnabled) {
		this.logScaleEnabled = logScaleEnabled;
	}

	/**
	 * @param majorTickMarkStepHint
	 *            the majorTickMarkStepHint to set, should be less than 1000.
	 */
	public void setMajorTickMarkStepHint(int majorTickMarkStepHint) {
		this.majorTickMarkStepHint = majorTickMarkStepHint;
		setDirty(true);
		revalidate();
		repaint();
	}

	/**
	 * @param minorTicksVisible
	 *            the minorTicksVisible to set
	 */
	public void setMinorTicksVisible(boolean minorTicksVisible) {
		this.minorTicksVisible = minorTicksVisible;
	}

	/** set the scale range */
	public void setRange(final Range range) {
		if (range == null) {
			SWT.error(SWT.ERROR_NULL_ARGUMENT);
			return; // to suppress warnings...
		}
		setRange(range.getLower(), range.getUpper());
	}

	/**
	 * Set the range with option to honor its original direction.
	 * 
	 * @param t1
	 *            value 1 of the range
	 * @param t2
	 *            value 2 of the range
	 * @param honorOriginDirection
	 *            if true, the start and end value of the range will set
	 *            according to its original direction.
	 */
	public void setRange(double t1, double t2, boolean honorOriginDirection) {
		if (honorOriginDirection) {
			if (getRange().isMinBigger()) {
				setRange(t1 > t2 ? t1 : t2, t1 > t2 ? t2 : t1);
			} else
				setRange(t1 > t2 ? t2 : t1, t1 > t2 ? t1 : t2);
		} else
			setRange(t1, t2);
	}

	/**
	 * set the scale range
	 * 
	 * @param lower
	 *            the lower limit
	 * @param upper
	 *            the upper limit
	 * @throws IllegalArgumentException
	 *             if lower or upper is Nan of Infinite, or lower >= upper or
	 *             (upper - lower) is Infinite
	 */
	public void setRange(double lower, double upper) {
		if (Double.isNaN(lower) || Double.isNaN(upper) || Double.isInfinite(lower) || Double.isInfinite(upper)) {
			throw new IllegalArgumentException("Illegal range: lower=" + lower + ", upper=" + upper);
		}

		if (lower == upper) {
			upper = lower + 1;
			if (Double.isInfinite(upper))
				throw new IllegalArgumentException("Illegal range: lower=" + lower + ", upper=" + upper);
		}

		if (logScaleEnabled && lower <= 0) {
			lower = DEFAULT_LOG_SCALE_MIN;
		}

		min = lower;
		max = upper;

		// calculate the default decimal format
		if (formatPattern == null || formatPattern == default_decimal_format) {
			default_decimal_format = createDefaultDecimalFormat(min, max);
			formatPattern = default_decimal_format;
			autoFormat = true;
		}

		if (formatPattern.equals(default_decimal_format) || formatPattern.equals(DEFAULT_ENGINEERING_FORMAT)) {
			if ((max != 0 && Math.abs(Math.log10(Math.abs(max))) >= ENGINEERING_LIMIT)
					|| (min != 0 && Math.abs(Math.log10(Math.abs(min))) >= ENGINEERING_LIMIT))
				formatPattern = DEFAULT_ENGINEERING_FORMAT;
			else
				formatPattern = default_decimal_format;
			autoFormat = true;
		}
		range = new Range(min, max);
		setDirty(true);
		revalidate();
		repaint();
	}

	static String createDefaultDecimalFormat(double min, double max) {
		double f = LargeNumberUtils.maxMagnitude(min, max);
		double mantissa = Math.abs(max / f - min / f);
		String decimalFormat;
		if (mantissa > 0.1/f) {
			decimalFormat = "############.##";
		} else {
			decimalFormat = "##.##";
			while (mantissa < 1./f) {
				mantissa *= 10.0;
				decimalFormat += "#";
			}
		}
		return decimalFormat;
	}

	protected void internalSetRange(Range range) {
		this.range = range;
	}

	/**
	 * @param scaleLineVisible
	 *            the scaleLineVisible to set
	 */
	public void setScaleLineVisible(boolean scaleLineVisible) {
		this.scaleLineVisible = scaleLineVisible;
	}

	/**
	 * @param tickLabelSide
	 *            the side of the tick label relative to tick mark
	 */
	public void setTickLabelSide(LabelSide tickLabelSide) {
		this.tickLabelSide = tickLabelSide;
		revalidate();
	}

	/**
	 * @deprecated use correctly spelled {@link #setTickLabelSide(LabelSide)}
	 */
	@Deprecated
	public void setTickLableSide(LabelSide tickLabelSide) {
		setTickLabelSide(tickLabelSide);
	}

	/**
	 * Set the time unit for a date enabled scale. The format of the time would
	 * be determined by it.
	 * 
	 * @param timeUnit
	 *            the timeUnit to set. It should be one of:
	 *            <tt>Calendar.MILLISECOND</tt>, <tt>Calendar.SECOND</tt>,
	 *            <tt>Calendar.MINUTE</tt>, <tt>Calendar.HOUR_OF_DAY</tt>,
	 *            <tt>Calendar.DATE</tt>, <tt>Calendar.MONTH</tt>,
	 *            <tt>Calendar.YEAR</tt>.
	 * @see Calendar
	 */
	public void setTimeUnit(int timeUnit) {
		this.timeUnit = timeUnit;
		setDirty(true);
	}

	/**
	 * Updates the tick, recalculate all inner parameters
	 */
	public abstract void updateTick();

	/**
	 * @param majorGridStep
	 *            the majorGridStep to set
	 */
	public void setMajorGridStep(double majorGridStep) {
		this.majorGridStep = majorGridStep;
		setDirty(true);
	}

	/**
	 * @return the majorGridStep
	 */
	public double getMajorGridStep() {
		return majorGridStep;
	}

	/**
	 * @param minorTickMarkStepHint
	 *            the minorTickMarkStepHint to set
	 */
	public void setMinorTickMarkStepHint(int minorTickMarkStepHint) {
		this.minorTickMarkStepHint = minorTickMarkStepHint;
	}

	/**
	 * @return the minorTickMarkStepHint
	 */
	public int getMinorTickMarkStepHint() {
		return minorTickMarkStepHint;
	}

	/**
	 * @param autoFormat
	 *            the autoFormat to set
	 */
	public void setAutoFormat(boolean autoFormat) {
		internalSetAutoFormat(autoFormat);
		if (autoFormat) {
			formatPattern = null;
			setRange(getRange());
			format(0);
		}
	}

	/**
	 * Sets ONLY the autoFormat value
	 *
	 * @param autoFormat
	 */
	protected void internalSetAutoFormat(boolean autoFormat) {
		this.autoFormat = autoFormat;
	}

	/**
	 * @return the autoFormat
	 */
	public boolean isAutoFormat() {
		return autoFormat;
	}

}
